package com.frank.minio.upload.service.impl;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.frank.minio.upload.mapper.SysUploadTaskMapper;
import com.frank.minio.upload.model.dto.TaskInfoDTO;
import com.frank.minio.upload.model.dto.TaskRecordDTO;
import com.frank.minio.upload.model.entity.BigFile;
import com.frank.minio.upload.model.entity.Range;
import com.frank.minio.upload.model.entity.SysUploadTask;
import com.frank.minio.upload.model.error.GlobalException;
import com.frank.minio.upload.model.param.InitTaskParam;
import com.frank.minio.upload.properties.MinioProperties;
import com.frank.minio.upload.service.MinioUtils;
import com.frank.minio.upload.service.SysUploadTaskService;
import io.minio.StatObjectResponse;
import io.minio.messages.Part;
import lombok.SneakyThrows;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.*;

/**
 * 分片上传-分片任务记录(SysUploadTask)表服务实现类
 *
 * @since 2022-08-22 17:47:31
 */
@Service("sysUploadTaskService")
public class SysUploadTaskServiceImpl extends ServiceImpl<SysUploadTaskMapper, SysUploadTask> implements SysUploadTaskService {
    private static final Logger log = LoggerFactory.getLogger(SysUploadTaskServiceImpl.class);


    @Autowired
    private MinioUtils minioUtils;

    @Resource
    private MinioProperties minioProperties;

    @Resource
    private SysUploadTaskMapper sysUploadTaskMapper;

    @Override
    public SysUploadTask getByIdentifier(String identifier) {
        return sysUploadTaskMapper.selectOne(new QueryWrapper<SysUploadTask>().lambda().eq(SysUploadTask::getFileIdentifier, identifier));
    }


    @Override
    public TaskInfoDTO initTask(InitTaskParam param) {

        Date currentDate = new Date();
        String bucketName = minioProperties.getBucket();
        String fileName = param.getFileName();
        String suffix = fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length());
        String key = StrUtil.format("{}/{}.{}", DateUtil.format(currentDate, "YYYY-MM-dd"), IdUtil.randomUUID(), suffix);
        String contentType = MediaTypeFactory.getMediaType(key).orElse(MediaType.APPLICATION_OCTET_STREAM).toString();
        int chunkNum = (int) Math.ceil(param.getTotalSize() * 1.0 / param.getChunkSize());
        Map<String, Object> map = minioUtils.initMultiPartUpload(bucketName, key, chunkNum);
        String uploadId = String.valueOf(map.get("uploadId"));

        SysUploadTask task = new SysUploadTask();

        task.setBucketName(minioProperties.getBucket())
                .setChunkNum(chunkNum)
                .setChunkSize(param.getChunkSize())
                .setTotalSize(param.getTotalSize())
                .setFileIdentifier(param.getIdentifier())
                .setFileName(fileName)
                .setObjectKey(key)
                .setUploadId(uploadId);
        sysUploadTaskMapper.insert(task);
        return new TaskInfoDTO().setFinished(false).setTaskRecord(TaskRecordDTO.convertFromEntity(task)).setPath(getPath(bucketName, key)).setUploadUrlInfo(map);
    }

    @Override
    public String getPath(String bucket, String objectKey) {
        return StrUtil.format("{}/{}/{}", minioProperties.getEndpoint(), bucket, objectKey);
    }

    @SneakyThrows
    @Override
    public TaskInfoDTO getTaskInfo(String identifier) {
        SysUploadTask task = getByIdentifier(identifier);
        if (task == null) {
            return null;
        }
        TaskInfoDTO result = new TaskInfoDTO().setFinished(true).setTaskRecord(TaskRecordDTO.convertFromEntity(task)).setPath(getPath(task.getBucketName(), task.getObjectKey()));

        boolean doesObjectExist = minioUtils.isFileExist(task.getBucketName(), task.getObjectKey());
        if (!doesObjectExist) {
            // 未上传完，返回已上传的分片
//            Set<Integer> integers = minioUtils.uploadFileFinish(task.getFileIdentifier());
//            result.setFinished(false).getTaskRecord().setExitParts(integers);
            List<Part> parts = minioUtils.listParts(task.getBucketName(), task.getObjectKey(), task.getUploadId());
            result.setFinished(false).getTaskRecord().setExitPartList(parts);
        }
        return result;
    }

    @SneakyThrows
    @Override
    public String genPreSignUploadUrl(String bucket, String objectKey, Map<String, String> params) {
        return minioUtils.getPresignedObjectUrl(bucket, objectKey, params);
    }

    @SneakyThrows
    @Override
    public void merge(String identifier) {
        SysUploadTask task = getByIdentifier(identifier);
        if (task == null) {
            throw new RuntimeException("分片任务不存");
        }
        List<Part> parts = minioUtils.listParts(task.getBucketName(), task.getObjectKey(), task.getUploadId());
        if (!task.getChunkNum().equals(parts.size())) {
            // 已上传分块数量与记录中的数量不对应，不能合并分块
            throw new RuntimeException("分片缺失，请重新上传");
        }
        minioUtils.mergeMultipartUpload(task.getBucketName(), task.getObjectKey(), task.getUploadId());

    }

    @Override
    public void uploadBigFile(BigFile bigFile) {
        SysUploadTask task = getByIdentifier(bigFile.getIdentifier());
        if (task == null) {
            throw new RuntimeException("分片任务不存在");
        }
        try {
            MultipartFile file = bigFile.getFile();
            Integer currIndex = bigFile.getCurrIndex();
            Integer totalPieces = bigFile.getTotalPieces();
            String md5 = bigFile.getIdentifier();
            log.info("[Bigfile] 上传文件md5: {} ,分片索引: {}", md5, currIndex);
            minioUtils.uploadFileFragment(file, currIndex, totalPieces, md5);

        } catch (Exception e) {
            e.printStackTrace();
            log.error("[Bigfile] 上传文件时出现异常");
            throw new GlobalException("上传文件时出现异常");
        }
    }

    @Override
    public String mergeByBig(String md5) {
        SysUploadTask task = getByIdentifier(md5);
        if (task == null) {
            throw new RuntimeException("分片任务不存在");
        }
        try {
            Set<Integer> parts = minioUtils.uploadFileFinish(task.getFileIdentifier());
            if (!task.getChunkNum().equals(parts.size())) {
                // 已上传分块数量与记录中的数量不对应，不能合并分块
                throw new RuntimeException("分片缺失，请重新上传");
            }
            // 发起文件合并请求, 无异常则成功
            minioUtils.composeFileFragment(minioProperties.getBucket(), task.getObjectKey(), task.getChunkNum(), task.getFileIdentifier());
            return task.getObjectKey();
        } catch (Exception e) {
            e.printStackTrace();
            throw new GlobalException("获取minio信息失败");
        }
    }

    @Override
    public void downloadFile(String identifier, HttpServletResponse response, String range) throws Exception {
        SysUploadTask task = getByIdentifier(identifier);
        if (task == null) {
            throw new RuntimeException("分片任务不存在");
        }
        StatObjectResponse statObjectResponse = minioUtils.statObject(task.getBucketName(), task.getObjectKey());
        String filename = task.getFileName();
        long length = statObjectResponse.size();
        Range full = new Range(0, length - 1, length);
        List<Range> ranges = new ArrayList<>();
        //处理Range
        try {
            dealRanges(full, range, ranges, response, length);
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("文件下载异常：" + e.getMessage());
        }
        // 如果浏览器支持内容类型，则设置为“内联”，否则将弹出“另存为”对话框. attachment inline
        String disposition = "attachment";

        // 将需要下载的文件段发送到客服端，准备流.
        try (ServletOutputStream output = response.getOutputStream()) {
            //最后修改时间
            long lastModified = LocalDateTime.ofInstant(statObjectResponse.lastModified().toInstant(),
                    ZoneId.of(ZoneId.systemDefault().getId())).toEpochSecond(ZoneOffset.UTC);
            //初始化response.
            response.reset();
            response.setBufferSize(20480);
            response.setHeader("Content-type", "application/octet-stream;charset=UTF-8");
            response.setHeader("Content-Disposition", disposition + ";filename=" +
                    URLEncoder.encode(filename, StandardCharsets.UTF_8.name()));
            response.setHeader("Accept-Ranges", "bytes");
            response.setHeader("ETag", URLEncoder.encode(filename, StandardCharsets.UTF_8.name()));
            response.setDateHeader("Last-Modified", lastModified);
            response.setDateHeader("Expires", System.currentTimeMillis() + 604800000L);
            //输出Range到response
            outputRange(task, response, ranges, output, full, length);
            output.flush();
            response.flushBuffer();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("文件下载异常：" + e.getMessage());
        }
    }

    /**
     * 处理请求中的Range(多个range或者一个range，每个range范围)
     *
     * @param range    :
     * @param ranges   :
     * @param response :
     * @param length   :
     * @author kevin
     * @date 2021/1/17
     */
    private void dealRanges(Range full, String range, List<Range> ranges, HttpServletResponse response,
                            long length) throws IOException {
        if (range != null) {
            // Range 头的格式必须为 "bytes=n-n,n-n,n-n...". 如果不是此格式, 返回 416.
            if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {
                response.setHeader("Content-Range", "bytes */" + length);
                response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                return;
            }

            // 处理传入的range的每一段.
            for (String part : range.substring(6).split(",")) {
                part = part.split("/")[0];
                // 对于长度为100的文件，以下示例返回:
                // 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100).
                int delimiterIndex = part.indexOf("-");
                long start = Range.sublong(part, 0, delimiterIndex);
                long end = Range.sublong(part, delimiterIndex + 1, part.length());

                //如果未设置起始点，则计算的是最后的 end 个字节；设置起始点为 length-end，结束点为length-1
                //如果未设置结束点，或者结束点设置的比总长度大，则设置结束点为length-1
                if (start == -1) {
                    start = length - end;
                    end = length - 1;
                } else if (end == -1 || end > length - 1) {
                    end = length - 1;
                }

                // 检查Range范围是否有效。如果无效，则返回416.
                if (start > end) {
                    response.setHeader("Content-Range", "bytes */" + length);
                    response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                    return;
                }
                // 添加Range范围.
                ranges.add(new Range(start, end - 1, length));
            }
        } else {
            //如果未传入Range，默认下载整个文件
            ranges.add(full);
        }
    }

    /**
     * output写流输出到response
     *
     * @param response :
     * @param ranges   :
     * @param output   :
     * @param full     :
     * @param length   :
     * @author kevin
     * @date 2021/1/17
     */
    private void outputRange(SysUploadTask task, HttpServletResponse response, List<Range> ranges,
                             ServletOutputStream output, Range full, long length) throws Exception {
        if (ranges.isEmpty() || ranges.get(0) == full) {
            // 返回整个文件.
            response.setContentType("application/octet-stream;charset=UTF-8");
            response.setHeader("Content-Range", "bytes " + full.getStart() + "-" + full.getEnd() + "/" + full.getTotal());
            response.setHeader("Content-length", String.valueOf(full.getLength()));
            response.setStatus(HttpServletResponse.SC_OK); // 200.
            Range.copy(minioUtils.getObject(task.getBucketName(), task.getObjectKey(), full.getStart(), full.getLength()), output, length, full.getStart(), full.getLength());
        } else if (ranges.size() == 1) {
            // 返回文件的一个分段.
            Range r = ranges.get(0);
            response.setContentType("application/octet-stream;charset=UTF-8");
            response.setHeader("Content-Range", "bytes " + r.getStart() + "-" + r.getEnd() + "/" + r.getTotal());
            response.setHeader("Content-length", String.valueOf(r.getLength()));
            response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.
            // 复制单个文件分段.
            Range.copy(minioUtils.getObject(task.getBucketName(), task.getObjectKey(), r.getStart(), r.getLength()), output, length, r.getStart(), r.getLength());
        } else {
            // 返回文件的多个分段.
            response.setContentType("multipart/byteranges; boundary=MULTIPART_BYTERANGES");
            response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.

            // 复制多个文件分段.
            for (Range r : ranges) {
                //为每个Range添加MULTIPART边界和标题字段
                output.println();
                output.println("--MULTIPART_BYTERANGES");
                output.println("Content-Type: application/octet-stream;charset=UTF-8");
                output.println("Content-length: " + r.getLength());
                output.println("Content-Range: bytes " + r.getStart() + "-" + r.getEnd() + "/" + r.getTotal());
                // 复制多个需要复制的文件分段当中的一个分段.
                Range.copy(minioUtils.getObject(task.getBucketName(), task.getObjectKey(), r.getStart(), r.getLength()), output, length, r.getStart(), r.getLength());
            }

            // 以MULTIPART文件的边界结束.
            output.println();
            output.println("--MULTIPART_BYTERANGES--");
        }
    }

}
